﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using Antlr.Runtime;
using Antlr.Runtime.Tree;
using SimpleSpreadsheet.Core;
using SimpleSpreadsheet.Core.Exceptions;
using SimpleSpreadsheet.Core.Expressions;
using SimpleSpreadsheet.Core.Parser;

namespace SimpleSpreadsheet.Expressions
{
    /// <summary>
    /// Represents the parsed and evaluatable expression
    /// </summary>
    public class Expression : INotifyPropertyChanged
    {
        private readonly IDictionary<string, IFunction> _formulas;
        private readonly BaseExpression _parsedExpression;

        public Expression(string expr, IDictionary<string, IFunction> formulas, ExtendedParserBase parser)
        {
            if (string.IsNullOrEmpty(expr))
                throw new ArgumentException("Expression can't be empty", "expr");

            References = new HashSet<string>();
            ReferenceValues = new Dictionary<string, decimal>();
            _formulas = formulas;
            ExpressionText = expr;
            _parsedExpression = BaseExpression.Create(Parse(parser), p => References.Add(p.Name));
        }

        public IValueProvider ReferenceValueProvider { get; set; }
        public string ExpressionText { get; private set; }
        public HashSet<string> References { get; private set; }
        public IDictionary<string, decimal> ReferenceValues { get; set; }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        public event EvaluateParameterHandler EvaluateParameter;

        /// <summary>
        /// Static method for expression text validation
        /// </summary>
        /// <param name="expression"></param>
        public static bool Validate(string expression)
        {
            var lexer = new ExpressionLexer(new ANTLRStringStream(expression));
            var parser = new ExpressionParser(new CommonTokenStream(lexer));

            try
            {
                expression_return expressionReturn = parser.Expression();
                CommonTree tree = expressionReturn.Tree;
                if (parser.HasError)
                    return false;
                    //throw new ParseException("Could not parse expression",
                    //                         new EvaluationException(parser.ErrorMessage + " " + parser.ErrorPosition));

                return true;
            }
            catch (EvaluationException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new EvaluationException(e.Message, e);
            }
        }

        /// <summary>
        /// Parse the specified expression text using provided lexer and parser
        /// </summary>
        /// <param name="parser"></param>
        /// <returns></returns>
        private CommonTree Parse(ExtendedParserBase parser)
        {
            try
            {
                expression_return expressionReturn = parser.Expression();
                CommonTree tree = expressionReturn.Tree;
                if (parser.HasError)
                    throw new EvaluationException(parser.ErrorMessage + " " + parser.ErrorPosition);

                return tree;
            }
            catch (EvaluationException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new EvaluationException(e.Message, e);
            }
        }

        /// <summary>
        /// Evaluates the expression using specified reference values (using ReferenceValueProvider)
        /// </summary>
        /// <returns></returns>
        public object Evaluate()
        {
            var visitor = new ExpressionEvaluator(_formulas);
            visitor.EvaluateParameter += EvaluateParameter;
            visitor.References = ReferenceValues;

            PopulateParameters(ReferenceValueProvider);
            _parsedExpression.Accept(visitor);
            return visitor.Result;
        }

        /// <summary>
        /// Fills the values of references parameters (cells) using specified provider (e.g. worksheet)
        /// </summary>
        /// <param name="provider"></param>
        public void PopulateParameters(IValueProvider provider)
        {
            foreach (string reference in References)
                ReferenceValues[reference] = provider.GetValueByName(reference);
        }

        /// <summary>
        /// INotifyPropertyChanged implementation
        /// </summary>
        /// <param name="propertyName"></param>
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}